from typing import Dict, List, Tuple
from collections import defaultdict, OrderedDict
from cache import LRUCacheF
import numpy as np
from data import Content, Slice, UserState, KV

import tools
from data import WatchEvent

class Edge:
    def __init__(
            self, videos: Dict[str, Dict], 
            user_preferences: List[np.ndarray], 
            record_table: List[Dict[int, Tuple[int, int, UserState]]],
            cache_capacity: int = 100,
            W: int = 10) -> None:
        self.videos = videos
        self.user_preferences = user_preferences
        self.concurrent: Dict[int, List[Tuple[int, int]]] = defaultdict(list)
        self.record_table = record_table
        self.cache = LRUCacheF(cache_capacity)
        self.cache_hit = 0
        self.cache_miss = 0
        self.first_delay = 0
        self.first_watch = 0
        self.cache_replace = 0
        self.backhaul_traffic = 0
        self.online_users = 0
        self.W = W
        self.clock = 0
    
    def has_watched(self, uid: int, vid: int) -> bool:
        return vid in self.record_table[uid]

    def notify(self, event: WatchEvent) -> None:
        self.online_users += 1
        self.concurrent[event.vid].append((event.uid, event.vdx))

    def get_reco_list(self, uid: int, upd: float=1) -> List[int]:
        L_rec_o = self.gen_reco_list(uid)
        L_cached = [content.vid for content in self.cache.items()]
        L_cached = [vid for vid in L_cached if not self.has_watched(uid, vid)]
        adjust = self.adjust_reco_list(uid, L_rec_o, L_cached, upd)
        return adjust

    def gen_reco_list(self, uid: int) -> List[int]:
        pref = self.user_preferences[uid]
        candidate = np.argsort(-tools.cosine_similarity(pref, self.videos["feature"]))
        reco_list = []
        for vid in candidate:
            if not self.has_watched(uid, vid):
                reco_list.append(vid)
            if len(reco_list) == self.W:
                break
        return reco_list

    def adjust_reco_list(self, uid: int, L_rec_o: List[int], L_cached: List[int], upd: float) -> List[int]:
        pref = self.user_preferences[uid]
        Lo = sorted([KV(int(vid), tools.cosine_similarity(pref, self.videos["feature"][vid])[0], 'o') for vid in L_rec_o], reverse=True)

        if len(L_cached) == 0:
            return L_rec_o
        
        Lc = sorted([KV(vid, tools.cosine_similarity(pref, self.videos["feature"][vid])[0], 'c') for vid in L_cached], reverse=True)

        best = np.mean([x.val for x in Lo])

        Lf = []
        idx = len(Lc) - 1
        while idx >= 0:
            item = Lc[idx]
            if item in Lo:
                item.typ = 'c'
                Lf.append(item)
                Lc.pop(idx)
            idx -= 1

        d = 1
        idx = len(Lo) - 1
        while len(Lf) < self.W and idx >= 0:
            item = Lo[idx]
            if item not in Lf:
                e = item.val
                a = Lc[0].val
                d = 1 - ((e - a) / (self.W * best))
                if d >= upd:
                    Lc[0].typ = 'c'
                    Lf.append(Lc[0])
                    Lc.pop(0)
                else:
                    item.typ = 'o'
                    Lf.append(item)
            idx -= 1

        return [kv.vid for kv in sorted(Lf, reverse=True)]

    def lru_f(self) -> None:
        concurrent = OrderedDict(sorted(self.concurrent.items(), key=lambda x: len(x[1]), reverse=True))
        for vid, pkg in concurrent.items():
            vlen = int(self.videos["duration"][vid])
            for uid, vdx in pkg:
                miss = not self.cache.get(Content(vid, Slice(vdx, vdx)))
                self.first_watch += (1 if vdx == 1 else 0)
                if miss:
                    self.cache_miss += 1
                    self.first_delay += (1 if vdx == 1 else 0)
                    self.cache_replace += 1
                    self.cache.put(Content(vid, Slice(1, vlen)))
                    self.backhaul_traffic += vlen
                else:
                    self.cache_hit += 1
        return None

    def clear(self) -> None:
        self.concurrent.clear()
        self.online_users = 0

    def step(self) -> None:
        self.lru_f()
        self.clock += 1
